﻿using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Timers;

namespace FMOD
{
    public class AudioTrack : IDisposable, INotifyPropertyChanged
    {
        public const int DefaultDecimationInterval = 500;
        private readonly string _file;

        private readonly Timer _positionReportingTimer;
        private int _numChannels;
        private Channel _playbackChannel;
        private Sound _playbackSound;
        private float _sampleRate;
        private int _soundBits;
        private SOUND_FORMAT _soundFormat = SOUND_FORMAT.NONE;
        private SOUND_TYPE _soundType = SOUND_TYPE.UNKNOWN;
        private FmodSystem _system;
        private Channel _viewChannel;
        private Sound _viewSound;

        private CHANNEL_CALLBACK _channelCallback;

        public AudioTrack(string file)
        {
            _file = file;
            _positionReportingTimer = new Timer(25) {Enabled = false, AutoReset = true};
            _positionReportingTimer.Elapsed += ReportingTimerElapsed;
            Load();
        }

        public bool Paused
        {
            get
            {
                if (_playbackChannel == null)
                    return false;

                bool paused = false;
                RESULT result = _playbackChannel.getPaused(ref paused);
                ThrowOnBadResult(result);
                return paused;
            }

            set
            {
                if (value != Paused)
                {
                    RESULT result = _playbackChannel.setPaused(value);
                    ThrowOnBadResult(result);
                    OnPropertyChanged("Paused");
                    _positionReportingTimer.Enabled = !value;
                }
            }
        }

        public TimeSpan Position
        {
            get
            {
                uint ms = 0;
                RESULT result = _playbackChannel.getPosition(ref ms, TIMEUNIT.MS);
                if (result != RESULT.ERR_INVALID_HANDLE)
                    ThrowOnBadResult(result);
                return TimeSpan.FromMilliseconds(ms);
            }

            set
            {
                if (value > Length)
                    throw new ArgumentOutOfRangeException("value", "Must be less than the sound length");
                if (value < TimeSpan.Zero)
                    throw new ArgumentOutOfRangeException("value", "Cannot be negative");

                RESULT result = _playbackChannel.setPosition((uint) value.TotalMilliseconds, TIMEUNIT.MS);
                ThrowOnBadResult(result);
                OnPositionChanged();
            }
        }

        public TimeSpan Length
        {
            get
            {
                uint ms = 0;
                RESULT result = _playbackSound.getLength(ref ms, TIMEUNIT.MS);
                ThrowOnBadResult(result);
                return TimeSpan.FromMilliseconds(ms);
            }
        }

        public uint TotalSamples
        {
            get
            {
                uint samples = 0;
                RESULT result = _playbackSound.getLength(ref samples, TIMEUNIT.PCM);
                ThrowOnBadResult(result);
                return samples;
            }
        }

        public uint TotalSampleBytes
        {
            get
            {
                uint bytes = 0;
                RESULT result = _playbackSound.getLength(ref bytes, TIMEUNIT.PCMBYTES);
                ThrowOnBadResult(result);
                return bytes;
            }
        }

        public float SampleRate
        {
            get { return _sampleRate; }
        }

        public int Bits
        {
            get { return _soundBits; }
        }

        public uint BytesPerSample { get; private set; }

        public float Volume
        {
            get
            {
                float vol = 0;
                RESULT result = _playbackChannel.getVolume(ref vol);
                ThrowOnBadResult(result);
                return vol;
            }

            set
            {
                if (value > 1.0f || value < 0.0f)
                    throw new ArgumentOutOfRangeException("value", "Must be between 0.0 and 1.0");

                if (value != Volume)
                {
                    RESULT result = _playbackChannel.setVolume(value);
                    ThrowOnBadResult(result);
                    OnPropertyChanged("Volume");
                }
            }
        }

        public int ChannelCount
        {
            get { return _numChannels; }
        }

        #region IDisposable Members

        public void Dispose()
        {
            RESULT result;

            if (_playbackSound != null)
            {
                result = _playbackSound.release();
                ThrowOnBadResult(result);
            }
            if (_system != null)
            {
                result = _system.close();
                ThrowOnBadResult(result);
                result = _system.release();
                ThrowOnBadResult(result);
            }
        }

        #endregion

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        private void Load()
        {
            RESULT result = Factory.System_Create(ref _system);
            ThrowOnBadResult(result);
            result = _system.init(2, INITFLAGS.NORMAL, (IntPtr) null);
            ThrowOnBadResult(result);
            result = _system.createStream(_file, MODE._2D | MODE.HARDWARE | MODE.CREATESTREAM | MODE.ACCURATETIME,
                                          ref _viewSound);
            ThrowOnBadResult(result);
            result = _system.playSound(CHANNELINDEX.FREE, _viewSound, true, ref _viewChannel);
            ThrowOnBadResult(result);
            result = _system.createStream(_file, MODE._2D | MODE.HARDWARE | MODE.CREATESTREAM | MODE.OPENONLY,
                                          ref _playbackSound);
            ThrowOnBadResult(result);
            result = _system.playSound(CHANNELINDEX.FREE, _playbackSound, true, ref _playbackChannel);
            ThrowOnBadResult(result);
            _channelCallback = new CHANNEL_CALLBACK(ChannelCallback);
            result = _playbackChannel.setCallback(_channelCallback);
            ThrowOnBadResult(result);
            Analyze();
        }

        private RESULT ChannelCallback(IntPtr channelraw, CHANNEL_CALLBACKTYPE type, IntPtr commanddata1,
                                       IntPtr commanddata2)
        {
            switch (type)
            {
                case CHANNEL_CALLBACKTYPE.END:
                    _positionReportingTimer.Enabled = false;
                    Load();
                    if (Stopped != null)
                        Stopped(this, EventArgs.Empty);
                    break;
            }
            return RESULT.OK;
        }

        private void Analyze()
        {
            RESULT result = _viewSound.getFormat(ref _soundType, ref _soundFormat, ref _numChannels, ref _soundBits);
            ThrowOnBadResult(result);

            BytesPerSample = (uint) (_soundBits*_numChannels/8);

            float volume = 0;
            float pan = 0;
            int priority = 0;

            _viewSound.getDefaults(ref _sampleRate, ref volume, ref pan, ref priority);
        }

        private void ReportingTimerElapsed(object sender, ElapsedEventArgs e)
        {
            _system.update();
            OnPositionChanged();
        }

        public void Stop()
        {
            Paused = true;
            Position = TimeSpan.Zero;
            if (Stopped != null)
                Stopped(this, EventArgs.Empty);
        }

        public Snippet GetSnippet(TimeSpan start, TimeSpan length)
        {
            Snippet snippt = GetSnippetInternal(start, length);
            snippt.Process(DefaultDecimationInterval);
            return snippt;
        }

        public Snippet GetUnprocessedSnippet(TimeSpan start, TimeSpan length)
        {
            return GetSnippetInternal(start, length);
        }

        private Snippet GetSnippetInternal(TimeSpan start, TimeSpan length)
        {
            var startSample = (uint) ToSamples(start);
            var samples = (uint) ToSamples(length);
            var readLength = (int) (samples*BytesPerSample);
            var block = new byte[readLength];
            uint read = 0;

            IntPtr blockPtr = Marshal.AllocHGlobal(readLength);

            try
            {
                var snippet = new Snippet(start, Bits, SampleRate, ChannelCount);

                if (start > Length)
                    return snippet;

                RESULT result = _viewSound.seekData(startSample);
                if (result == RESULT.ERR_INVALID_POSITION)
                    return snippet;

                result = _viewSound.readData(blockPtr, (uint) readLength, ref read);

                if (result != RESULT.ERR_FILE_EOF)
                    ThrowOnBadResult(result);

                Marshal.Copy(blockPtr, block, 0, (int) read);

                if (block.Length != read)
                    Array.Resize(ref block, (int) read);

                snippet.AddPcmBlock(block);

                return snippet;
            }
            finally
            {
                Marshal.FreeHGlobal(blockPtr);
                Position = TimeSpan.Zero;
            }
        }

        public double ToSamples(TimeSpan time)
        {
            double totalSeconds = time.TotalMilliseconds/1000;
            return totalSeconds*_sampleRate;
        }

        public TimeSpan ToTime(double samples)
        {
            return TimeSpan.FromMilliseconds(samples*1000/(_sampleRate));
        }

        private static void ThrowOnBadResult(RESULT result)
        {
            if (result != RESULT.OK)
            {
                throw new FmodException(Error.String(result));
            }
        }

        protected void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

        public event EventHandler PositionChanged;

        protected void OnPositionChanged()
        {
            if (PositionChanged != null)
                PositionChanged(this, EventArgs.Empty);
        }

        public event EventHandler Stopped;
    }
}